#!/usr/bin/env node
import 'dotenv/config';
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import pc from 'picocolors';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import SimpleSSH from './ssh-client.js';

const argv = yargs(hideBin(process.argv))
  .option('debug', { type: 'boolean', default: false })
  .help(false)
  .parse();

const ts = () => new Date().toISOString().replace('T', ' ').replace('Z', '');
const fmtBytes = (n) => {
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
  return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
};

function loadConfig() {
  const required = ['SSH_HOST', 'SSH_USER'];
  const missing = required.filter((k) => !process.env[k]);
  if (missing.length) {
    console.log(pc.red('缺少必要环境变量: ' + missing.join(', ')));
    console.log(pc.yellow('请创建 .env 或通过环境变量提供 SSH 信息'));
    process.exit(1);
  }

  const exclude = (process.env.EXCLUDE_PATTERNS || 'node_modules,.git,.DS_Store,*.log,*.tmp')
    .split(',')
    .map((x) => x.trim())
    .filter(Boolean);

  const resolveWinPath = (p) => path.resolve(p).replace(/\\/g, '/');

  // 多目录映射：MAPPINGS="E:/proj/a=>/remote/a;E:/proj/b=>/remote/b"
  const mappings = (process.env.MAPPINGS || '')
    .split(';')
    .map((s) => s.trim())
    .filter(Boolean)
    .map((pair) => {
      const [l, r] = pair.split('=>').map((x) => x && x.trim());
      if (!l || !r) return null;
      return { localRoot: resolveWinPath(l), remoteRoot: r.replace(/\\/g, '/') };
    })
    .filter(Boolean);

  const cfg = {
    host: process.env.SSH_HOST,
    port: parseInt(process.env.SSH_PORT || '22', 10),
    username: process.env.SSH_USER,
    password: process.env.SSH_PASSWORD,
    privateKey: process.env.SSH_KEY_PATH ? fs.readFileSync(process.env.SSH_KEY_PATH) : undefined,
    passphrase: process.env.SSH_PASSPHRASE,

    localRoot: resolveWinPath(process.env.LOCAL_ROOT || '../gs_demo_thinkphp/addons/gs_demo'),
    remoteRoot: (process.env.REMOTE_ROOT || '/addons/gs_demo').replace(/\\/g, '/'),
    watchMode: process.env.WATCH_MODE !== 'false',
    syncDelay: parseInt(process.env.SYNC_DELAY || '200', 10),
    syncDelete: process.env.SYNC_DELETE !== 'false',
    excludePatterns: exclude,

    remoteOwner: process.env.REMOTE_OWNER || '',
    remoteGroup: process.env.REMOTE_GROUP || '',
    remoteMode: process.env.REMOTE_MODE || '', // 如 664 或 644
    useSudo: process.env.USE_SUDO === 'true',
  };

  // 若提供了多映射，则覆盖为 mappings；否则用单个 root 兼容
  cfg.mappings = mappings.length
    ? mappings
    : [{ localRoot: cfg.localRoot, remoteRoot: cfg.remoteRoot }];

  return cfg;
}

function shouldExclude(file, patterns) {
  const unix = file.replace(/\\/g, '/');
  return patterns.some((p) => {
    if (p.endsWith('/')) return unix.includes(p.slice(0, -1));
    if (p.startsWith('*.')) return unix.endsWith(p.slice(1));
    return unix.includes(p);
  });
}

async function main() {
  const config = loadConfig();

  console.log(pc.cyan('SSH 同步工具启动'));
  if (config.mappings && config.mappings.length > 1) {
    console.log(pc.gray('多目录映射:'));
    for (const m of config.mappings) {
      console.log(pc.gray(` - 本地: ${m.localRoot}`));
      console.log(pc.gray(`   远程: ${m.remoteRoot}`));
    }
  } else {
    console.log(pc.gray(`本地: ${config.localRoot}`));
    console.log(pc.gray(`远程: ${config.remoteRoot}`));
  }

  const ssh = new SimpleSSH({
    host: config.host,
    port: config.port,
    username: config.username,
    password: config.password,
    privateKey: config.privateKey,
    passphrase: config.passphrase,
    readyTimeout: 30000,
  });

  console.log(pc.gray(`[${ts()}] 正在连接 SSH ${config.username}@${config.host}:${config.port} ...`));
  await ssh.connect();
  console.log(pc.green(`[${ts()}] SSH 连接成功`));

  const findMappingFor = (localPath) => {
    const unix = localPath.replace(/\\/g, '/');
    // 选择匹配到的最长 localRoot（避免嵌套路径冲突）
    let best = null;
    for (const m of config.mappings) {
      const rel = path.relative(m.localRoot, unix).replace(/\\/g, '/');
      if (!rel.startsWith('..')) {
        if (!best || m.localRoot.length > best.localRoot.length) best = m;
      }
    }
    return best;
  };

  const toRemotePath = (localPath) => {
    const mapping = findMappingFor(localPath);
    if (!mapping) return null;
    const rel = path.relative(mapping.localRoot, localPath).replace(/\\/g, '/');
    return `${mapping.remoteRoot}/${rel}`.replace(/\/+/g, '/');
  };

  const upload = async (localPath) => {
    try {
      if (!fs.existsSync(localPath) || fs.lstatSync(localPath).isDirectory()) return;
      const remotePath = toRemotePath(localPath);
      if (!remotePath) return;
      const size = fs.statSync(localPath).size;
      const start = Date.now();
      console.log(pc.gray(`[${ts()}] 开始上传: `) + pc.white(remotePath) + pc.gray(` (${fmtBytes(size)})`));
      await ssh.uploadFile(localPath, remotePath);
      // 可选：设置权限与所有者
      const cmds = [];
      if (config.remoteMode) cmds.push(`${config.useSudo ? 'sudo ' : ''}chmod ${config.remoteMode} "${remotePath.replace(/"/g, '\\"')}"`);
      if (config.remoteOwner || config.remoteGroup) {
        const ownerSpec = `${config.remoteOwner || ''}${config.remoteGroup ? ':' + config.remoteGroup : ''}`;
        if (ownerSpec) cmds.push(`${config.useSudo ? 'sudo ' : ''}chown ${ownerSpec} "${remotePath.replace(/"/g, '\\"')}"`);
      }
      for (const cmd of cmds) {
        try {
          if (argv.debug) console.log(pc.gray(`[${ts()}] 执行权限命令: ${cmd}`));
          const res = await ssh.exec(cmd);
          if (argv.debug) {
            if (res.stdout) console.log(pc.gray(`[${ts()}] 权限命令输出: ${res.stdout.trim()}`));
            if (res.stderr) console.log(pc.gray(`[${ts()}] 权限命令错误: ${res.stderr.trim()}`));
          }
        } catch (e) {
          console.log(pc.yellow(`[${ts()}] 权限设置失败: ${cmd} -> ${e.message}`));
        }
      }
      const ms = Date.now() - start;
      console.log(pc.green(`[${ts()}] 上传完成: `) + pc.white(remotePath) + pc.gray(` (${fmtBytes(size)}, ${ms} ms)`));
    } catch (e) {
      console.log(pc.red(`[${ts()}] 上传失败: `) + (argv.debug ? (e.stack || e.message) : e.message));
    }
  };

  const remove = async (localPath) => {
    try {
      const remotePath = toRemotePath(localPath);
      if (!remotePath) return;
      console.log(pc.gray(`[${ts()}] 开始删除: `) + pc.white(remotePath));
      await ssh.removeFile(remotePath);
      console.log(pc.yellow(`[${ts()}] 删除完成: `) + pc.white(remotePath));
    } catch (e) {
      console.log(pc.red(`[${ts()}] 删除失败: `) + (argv.debug ? (e.stack || e.message) : e.message));
    }
  };

  // 首次遍历并上传（可选：只在 FULL_SYNC=true 时）
  if (process.env.FULL_SYNC === 'true') {
    const walk = (dir, acc) => {
      for (const entry of fs.readdirSync(dir)) {
        const abs = path.join(dir, entry);
        if (shouldExclude(abs, config.excludePatterns)) continue;
        const stat = fs.lstatSync(abs);
        if (stat.isDirectory()) walk(abs, acc);
        else acc.push(abs);
      }
    };
    for (const m of config.mappings) {
      const toUpload = [];
      walk(m.localRoot, toUpload);
      for (const file of toUpload) await upload(file);
    }
  }

  // 监听模式
  if (config.watchMode) {
    for (const m of config.mappings) {
      const watcher = chokidar.watch(m.localRoot, {
        ignoreInitial: true,
        persistent: true,
        awaitWriteFinish: { stabilityThreshold: config.syncDelay, pollInterval: 50 },
      });

      watcher
        .on('add', (file) => {
          if (shouldExclude(file, config.excludePatterns)) return;
          console.log(pc.gray(`[${ts()}] 发现新文件: `) + pc.white(file));
          upload(file);
        })
        .on('change', (file) => {
          if (shouldExclude(file, config.excludePatterns)) return;
          console.log(pc.gray(`[${ts()}] 检测到改动: `) + pc.white(file));
          upload(file);
        })
        .on('unlink', (file) => {
          if (shouldExclude(file, config.excludePatterns)) return;
          console.log(pc.gray(`[${ts()}] 检测到删除: `) + pc.white(file));
          if (config.syncDelete) {
            remove(file);
          } else {
            console.log(pc.yellow(`[${ts()}] 已忽略远端删除（SYNC_DELETE=false）`));
          }
        });
    }

    console.log(pc.green('已启动监听（多目录），保存即上传，Ctrl+C 退出'));
  } else {
    console.log(pc.yellow('WATCH_MODE=false，未启动监听'));
  }
}

main().catch((e) => {
  console.error(pc.red(e.stack || e.message));
  process.exit(1);
});


